var slice = Array.prototype.slice;

function handleError(event, error, errbacks) {
    var i, l = errbacks.length;
    if (l > 0) {
        for (i = 0; i < l; ++i) {
            errbacks[i](event, error);
        }
        return;
    }
    setTimeout(function () {
        error.message = event + " listener threw error: " + error.message;
        throw error;
    }, 0);
}

function assertFunction(fn) {
    if (typeof fn !== "function") {
        throw new TypeError("Listener is not function");
    }
    return fn;
}

function supervisors(object) {
    if (!object.supervisors) {
        object.supervisors = [];
    }
    return object.supervisors;
}

function listeners(object, event) {
    if (!object.listeners) {
        object.listeners = {};
    }
    if (event && !object.listeners[event]) {
        object.listeners[event] = [];
    }
    return event ? object.listeners[event] : object.listeners;
}

function errbacks(object) {
    if (!object.errbacks) {
        object.errbacks = [];
    }
    return object.errbacks;
}

/**
 * @signature var emitter = bane.createEmitter([object]);
 *
 * Create a new event emitter. If an object is passed, it will be modified
 * by adding the event emitter methods (see below).
 */
function createEventEmitter(object) {
    object = object || {};

    function notifyListener(event, listener, args) {
        try {
            listener.listener.apply(listener.thisp || object, args);
        } catch (e) {
            handleError(event, e, errbacks(object));
        }
    }

    object.on = function (event, listener, thisp) {
        if (typeof event === "function") {
            return supervisors(this).push({
                listener: event,
                thisp: listener
            });
        }
        listeners(this, event).push({
            listener: assertFunction(listener),
            thisp: thisp
        });
    };

    object.off = function (event, listener) {
        var fns, events, i, l;
        if (!event) {
            fns = supervisors(this);
            fns.splice(0, fns.length);

            events = listeners(this);
            for (i in events) {
                if (events.hasOwnProperty(i)) {
                    fns = listeners(this, i);
                    fns.splice(0, fns.length);
                }
            }

            fns = errbacks(this);
            fns.splice(0, fns.length);

            return;
        }
        if (typeof event === "function") {
            fns = supervisors(this);
            listener = event;
        } else {
            fns = listeners(this, event);
        }
        if (!listener) {
            fns.splice(0, fns.length);
            return;
        }
        for (i = 0, l = fns.length; i < l; ++i) {
            if (fns[i].listener === listener) {
                fns.splice(i, 1);
                return;
            }
        }
    };

    object.once = function (event, listener, thisp) {
        var wrapper = function () {
            object.off(event, wrapper);
            listener.apply(this, arguments);
        };

        object.on(event, wrapper, thisp);
    };

    object.bind = function (object, events) {
        var prop, i, l;
        if (!events) {
            for (prop in object) {
                if (typeof object[prop] === "function") {
                    this.on(prop, object[prop], object);
                }
            }
        } else {
            for (i = 0, l = events.length; i < l; ++i) {
                if (typeof object[events[i]] === "function") {
                    this.on(events[i], object[events[i]], object);
                } else {
                    throw new Error("No such method " + events[i]);
                }
            }
        }
        return object;
    };

    object.emit = function (event) {
        var toNotify = supervisors(this);
        var args = slice.call(arguments),
            i, l;

        for (i = 0, l = toNotify.length; i < l; ++i) {
            notifyListener(event, toNotify[i], args);
        }

        toNotify = listeners(this, event).slice();
        args = slice.call(arguments, 1);
        for (i = 0, l = toNotify.length; i < l; ++i) {
            notifyListener(event, toNotify[i], args);
        }
    };

    object.errback = function (listener) {
        if (!this.errbacks) {
            this.errbacks = [];
        }
        this.errbacks.push(assertFunction(listener));
    };

    return object;
}

var bane = {
    createEventEmitter: createEventEmitter,
    aggregate: function (emitters) {
        var aggregate = createEventEmitter();
        emitters.forEach(function (emitter) {
            emitter.on(function (event, data) {
                aggregate.emit(event, data);
            });
        });
        return aggregate;
    }
};

var List = Immutable.List;
var Map = Immutable.Map;
var fromJS = Immutable.fromJS;

function partition(size, coll) {
    var res = [];
    for (var i = 0, l = coll.size || coll.length; i < l; i += size) {
        res.push(coll.slice(i, i + size));
    }
    return fromJS(res);
}

function identity(v) {
    return v;
}

function prop(n) {
    return function (object) {
        return object instanceof Map ? object.get(n) : object[n];
    };
}

function keep(list, pred) {
    return list.map(pred).filter(identity);
}

function repeat(n, val) {
    var res = [];
    while (n--) {
        res.push(val);
    }
    return List(res);
}

function shuffle(list) {
    return list.sort(function () {
        return Math.random() - 0.5;
    });
}

function initTiles(rows, cols, mines) {
    return shuffle(repeat(mines, Map({
        isMine: true,
        isRevealed: false
    })).concat(repeat(rows * cols - mines, Map({
        isRevealed: false
    })))).
        map(function (tile, idx) {
            return tile.set('id', idx);
        });
}

function onWEdge(game, tile) {
    return tile % game.get('cols') === 0;
}

function onEEdge(game, tile) {
    return tile % game.get('cols') === game.get('cols') - 1;
}

function idx(game, tile) {
    if (tile < 0) {
        return null;
    }
    return game.getIn(['tiles', tile]) ? tile : null;
}

function nw(game, tile) {
    return onWEdge(game, tile) ? null : idx(game, tile - game.get('cols') - 1);
}

function n(game, tile) {
    return idx(game, tile - game.get('cols'));
}

function ne(game, tile) {
    return onEEdge(game, tile) ? null : idx(game, tile - game.get('cols') + 1);
}

function e(game, tile) {
    return onEEdge(game, tile) ? null : idx(game, tile + 1);
}

function se(game, tile) {
    return onEEdge(game, tile) ? null : idx(game, tile + game.get('cols') + 1);
}

function s(game, tile) {
    return idx(game, tile + game.get('cols'));
}

function sw(game, tile) {
    return onWEdge(game, tile) ? null : idx(game, tile + game.get('cols') - 1);
}

function w(game, tile) {
    return onWEdge(game, tile) ? null : idx(game, tile - 1);
}

var directions = [nw, n, ne, e, se, s, sw, w];

function neighbours(game, tile) {
    return keep(directions, function (dir) {
        return game.getIn(['tiles', dir(game, tile)]);
    });
}

function getMineCount(game, tile) {
    var nbs = neighbours(game, tile);
    return nbs.filter(prop('isMine')).length;
}

function isMine(game, tile) {
    return game.getIn(['tiles', tile, 'isMine']);
}

function isSafe(game) {
    var tiles = game.get('tiles');
    var mines = tiles.filter(prop('isMine'));
    return mines.filter(prop('isRevealed')) === 0 &&
        tiles.length - mines.length === tiles.filter(prop('isRevealed')).length;
}

function isGameOver(game) {
    return isSafe(game) || game.get('isDead');
};

function addThreatCount(game, tile) {
    return game.setIn(['tiles', tile, 'threatCount'], getMineCount(game, tile));
}

function revealAdjacentSafeTiles(game, tile) {
    if (isMine(game, tile)) {
        return game;
    }
    game = addThreatCount(game, tile).setIn(['tiles', tile, 'isRevealed'], true);
    if (game.getIn(['tiles', tile, 'threatCount']) === 0) {
        return keep(directions, function (dir) {
            return dir(game, tile);
        }).reduce(function (game, pos) {
            return !game.getIn(['tiles', pos, 'isRevealed']) ?
                revealAdjacentSafeTiles(game, pos) : game;
        }, game);
    }
    return game;
}

function attemptWinning(game) {
    return isSafe(game) ? game.set('isSafe', true) : game;
}

function revealMine(tile) {
    return tile.get('isMine') ? tile.set('isRevealed', true) : tile;
}

function revealMines(game) {
    return game.updateIn(['tiles'], function (tiles) {
        return tiles.map(revealMine);
    });
}

function revealTile(game, tile) {
    var updated = !game.getIn(['tiles', tile]) ?
        game : game.setIn(['tiles', tile, 'isRevealed'], true);
    return isMine(updated, tile) ?
        revealMines(updated.set('isDead', true)) :
        attemptWinning(revealAdjacentSafeTiles(updated, tile));
};

function createGame(options) {
    return fromJS({
        cols: options.cols,
        rows: options.rows,
        playingTime: 0,
        tiles: initTiles(options.rows, options.cols, options.mines)
    });
};

var div = React.DOM.div;
var button = React.DOM.button;

// A little wrapper around React to avoid the slightly clunky factory wrapper
// around the component as well as the reliance on `this` to access props. The
// wrqpper defines a component as simply a render function.
function createComponent(render) {
    var component = React.createFactory(React.createClass({
        shouldComponentUpdate: function (newProps, newState) {
            // Simplified, this app only uses props
            return newProps.data !== this.props.data;
        },

        render: function () {
            return render.call(this, this.props.data);
        }
    }));

    return function (data) {
        return component({
            data: data
        });
    };
}

function createUI(channel) {
    var Tile = createComponent(function (tile) {
        if (tile.get('isRevealed')) {
            return div({
                className: 'tile' + (tile.get('isMine') ? ' mine' : '')
            },
                tile.get('threatCount') > 0 ? tile.get('threatCount') : '');
        }
        return div({
            className: 'tile',
            onClick: function () {
                channel.emit('reveal', tile.get('id'));
            }
        }, div({
            className: 'lid'
        }, ''));
    });

    var Row = createComponent(function (tiles) {
        return div({
            className: 'row'
        }, tiles.map(Tile).toJS());
    });

    var Board = createComponent(function (game) {
        return div({
            className: 'board'
        }, partition(game.get('cols'), game.get('tiles')).map(Row).toJS());
    });

    var UndoButton = createComponent(function () {
        return button({
            onClick: channel.emit.bind(channel, 'undo')
        }, 'Undo');
    });

    var Game = createComponent(function (game) {
        return div({}, [Board(game), UndoButton()]);
    });

    return function (data, container) {
        React.render(Game(data), container);
    };
}

// Start put game together
var channel = bane.createEventEmitter();

var renderMinesweeper = createUI(channel);
var game = createGame({
    cols: 16,
    rows: 16,
    mines: 48
});
var gameHistory = List([game]);

function render() {
    renderMinesweeper(game, document.getElementById('board'));
}

channel.on('undo', function () {
    if (gameHistory.size > 1) {
        gameHistory = gameHistory.pop();
        game = gameHistory.last();
        render();
    }
});

channel.on('reveal', function (tile) {
    if (isGameOver(game)) {
        return;
    }

    var newGame = revealTile(game, tile);

    if (newGame !== game) {
        gameHistory = gameHistory.push(newGame);
        game = newGame;
    }

    render();

    if (isGameOver(game)) {
        // Wait for the final render to complete before alerting the user
        setTimeout(function () {
            alert('GAME OVER!(游戏结束)');
        }, 50);
    }
});

render();